/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */

package com.aliyun.datahub.common.util;

import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
import java.util.Locale;
import java.util.SimpleTimeZone;

public class DateUtils {

    private static long TZ = +8;
    private static Calendar CAL = Calendar.getInstance();

    //Java unix time stamp of Current Timezone
    private static final long _0001_01_01 = getTime(1, 1, 1, 0, 0, 0);
    private static final long _0000_03_01 = getTime(0, 3, 1, 0, 0, 0);
    private static final long _1928_01_01 = getTime(1928, 1, 1, 0, 0, 0);
    private static final long _1940_06_03_01 = getTime(1940, 6, 3, 1, 0, 0);
    private static final long _1940_10_01 = getTime(1940, 10, 1, 0, 0, 0);
    private static final long _1941_03_16_01 = getTime(1941, 3, 16, 1, 0, 0);
    private static final long _1941_10_01 = getTime(1941, 10, 1, 0, 0, 0);
    private static final long _1986_05_04_01 = getTime(1986, 5, 4, 1, 0, 0);
    private static final long _1986_09_14 = getTime(1986, 9, 14, 0, 0, 0);
    private static final long _1987_04_12_01 = getTime(1987, 4, 12, 1, 0, 0);
    private static final long _1987_09_13 = getTime(1987, 9, 13, 0, 0, 0);
    private static final long _1988_04_10_01 = getTime(1988, 4, 10, 1, 0, 0);
    private static final long _1988_09_11 = getTime(1988, 9, 11, 0, 0, 0);
    private static final long _1989_04_16_01 = getTime(1989, 4, 16, 1, 0, 0);
    private static final long _1989_09_17 = getTime(1989, 9, 17, 0, 0, 0);
    private static final long _1990_04_15_01 = getTime(1990, 4, 15, 1, 0, 0);
    private static final long _1990_09_16 = getTime(1990, 9, 16, 0, 0, 0);
    private static final long _1991_04_14_01 = getTime(1991, 4, 14, 1, 0, 0);
    private static final long _1991_09_15 = getTime(1991, 9, 15, 0, 0, 0);

    //C unix time stamp of CST
    private static final long _0000_01_01_C = -62167248352L;
    private static final long _0000_03_01_C = -62162064352L;
    private static final long _0000_03_01_08_C = -62162035552L;
    private static final long _1927_12_31_23_59_59_C = -1325491553L;
    private static final long _1940_06_03_01_C = -933494400L;
    private static final long _1940_09_30_23_C = -923130000L;
    private static final long _1941_03_16_01_C = -908784000L;
    private static final long _1941_09_30_23_C = -891594000L;
    private static final long _1986_05_04_01_C = 515520000L;
    private static final long _1986_09_13_23_C = 527007600L;
    private static final long _1987_04_12_01_C = 545155200L;
    private static final long _1987_09_12_23_C = 558457200L;
    private static final long _1988_04_10_01_C = 576604800L;
    private static final long _1988_09_10_23_C = 589906800L;
    private static final long _1989_04_16_01_C = 608659200L;
    private static final long _1989_09_16_23_C = 621961200L;
    private static final long _1990_04_15_01_C = 640108800L;
    private static final long _1990_09_15_23_C = 653410800L;
    private static final long _1991_04_14_01_C = 671558400L;
    private static final long _1991_09_14_23_C = 684860400L;

    private static long getTime(int year, int month, int day, int hour, int min, int sec) {
        Calendar c = (Calendar) CAL.clone();


        c.set(Calendar.YEAR, year);
        c.set(Calendar.MONTH, month - 1);
        c.set(Calendar.DAY_OF_MONTH, day);
        c.set(Calendar.HOUR_OF_DAY, hour);
        c.set(Calendar.MINUTE, min);
        c.set(Calendar.SECOND, sec);
        c.set(Calendar.MILLISECOND, 0);
        return c.getTime().getTime();
    }

    public static long date2ms(Date date) {
        long ms;
        Calendar c = null;

        c = (Calendar) CAL.clone();
        c.setTime(date);
        ms = c.get(Calendar.MILLISECOND);

        return date2rawtime(date) * 1000 + ms;
    }

    public static Date ms2date(long ms) {
        Date d = rawtime2date(ms / 1000);
        Calendar c = (Calendar) CAL.clone();

        c.setTime(d);
        c.set(Calendar.MILLISECOND, (int) (ms % 1000));

        return c.getTime();
    }

    /**
     * @param date Object of Date Class
     * @return Unix Time Stamp
     * Java version of GLIBC mktime function
     * 
     * 1. This algorithm is design to convert date to unix timestamp.
     * The date "0000-03-01 00:00:00" is regarded as the beginning and other
     * dates are computed based on it.
     * 2. There is no parameter verification(assuming the parameter is legal
     * and unambiguous).
     */
    public static long date2rawtime(Date date) {
        //no input parameter verification
        //get literal value of broken-down time regard less of time zone
        Calendar c = null;
        long rawtime;
        int year, mon, day, hour, min, sec;
        long ans;

        c = (Calendar) CAL.clone();
        c.setTime(date);
        c.set(Calendar.MILLISECOND, 0);

        rawtime = date.getTime();
        year = c.get(Calendar.YEAR);
        if (rawtime < _0001_01_01) {
            year = 0;
        }
        mon = c.get(Calendar.MONTH) + 1;
        day = c.get(Calendar.DAY_OF_MONTH);
        hour = c.get(Calendar.HOUR_OF_DAY);
        min = c.get(Calendar.MINUTE);
        sec = c.get(Calendar.SECOND);

        if (rawtime < _0000_03_01) {
            return _0000_01_01_C + ((((mon - 1) * 31 + (day - 1)) * 24 + hour) * 60 + min) * 60 + sec;
        }

        //get literal value of calendar time regard less of time zone
        mon = mon - 2;
        if (mon <= 0) {
            mon += 12;
            year -= 1;
        }
        ans = year / 4 - year / 100 + year / 400 + 367 * mon / 12 + day;
        ans +=
                (year * 365
                        - 719499);   //719499 is a gap between 0000-03-01 00:00:00 and 1970-01-01 00:00:00 plus compensation of month
        ans = (((ans * 24 + hour) * 60 + min) * 60) + sec;

        //adjust for time zone
        ans = ans - TZ * 3600;

        //adjust for history
        if (rawtime < _1928_01_01) {
            ans = ans - 352;
        } else if (rawtime >= _1940_06_03_01 && rawtime < _1940_10_01) {
            ans = ans - 3600;
        } else if (rawtime >= _1941_03_16_01 && rawtime < _1941_10_01) {
            ans = ans - 3600;
        } else if (rawtime >= _1986_05_04_01 && rawtime < _1986_09_14) {
            ans = ans - 3600;
        } else if (rawtime >= _1987_04_12_01 && rawtime < _1987_09_13) {
            ans = ans - 3600;
        } else if (rawtime >= _1988_04_10_01 && rawtime < _1988_09_11) {
            ans = ans - 3600;
        } else if (rawtime >= _1989_04_16_01 && rawtime < _1989_09_17) {
            ans = ans - 3600;
        } else if (rawtime >= _1990_04_15_01 && rawtime < _1990_09_16) {
            ans = ans - 3600;
        } else if (rawtime >= _1991_04_14_01 && rawtime < _1991_09_15) {
            ans = ans - 3600;
        }

        return ans;
    }

    /**
     * @param rawtime Unix Time Stamp
     * @return Object of Date Class
     * Java version of GLIBC localtime function
     * 
     * 1. The algorithm is a reverse of mktime.
     * 2. There is no parameter verification(assuming the parameter is legal
     * and unambiguous).
     */
    public static Date rawtime2date(long rawtime) {
        int year, mon, day, hour, min, sec, leap;
        long offset;

        Calendar c = (Calendar) CAL.clone();

        if (rawtime < _0000_03_01_C) {
            offset = rawtime - _0000_01_01_C;
            sec = (int) (offset % 60);
            offset /= 60;
            min = (int) (offset % 60);
            offset /= 60;
            hour = (int) (offset % 24);
            offset /= 24;
            mon = (int) ((offset / 31) + 1);
            offset = offset % 31;
            day = (int) (offset + 1);
            year = 0;
        } else {
            offset = rawtime - _0000_03_01_08_C;
            if (rawtime > _1927_12_31_23_59_59_C) {
                offset = offset - 352;
            }
            if (rawtime >= _1940_06_03_01_C && rawtime < _1940_09_30_23_C) {
                offset = offset + 3600;
            } else if (rawtime >= _1941_03_16_01_C && rawtime < _1941_09_30_23_C) {
                offset = offset + 3600;
            } else if (rawtime >= _1986_05_04_01_C && rawtime < _1986_09_13_23_C) {
                offset = offset + 3600;
            } else if (rawtime >= _1987_04_12_01_C && rawtime < _1987_09_12_23_C) {
                offset = offset + 3600;
            } else if (rawtime >= _1988_04_10_01_C && rawtime < _1988_09_10_23_C) {
                offset = offset + 3600;
            } else if (rawtime >= _1989_04_16_01_C && rawtime < _1989_09_16_23_C) {
                offset = offset + 3600;
            } else if (rawtime >= _1990_04_15_01_C && rawtime < _1990_09_15_23_C) {
                offset = offset + 3600;
            } else if (rawtime >= _1991_04_14_01_C && rawtime < _1991_09_14_23_C) {
                offset = offset + 3600;
            }
            offset = offset + TZ * 60 * 60;

            sec = (int) (offset % 60);
            offset /= 60;
            min = (int) (offset % 60);
            offset /= 60;
            hour = (int) (offset % 24);
            offset /= 24;

            year = (int) (offset / 365);
            leap = year / 4 - year / 100 + year / 400;
            while (year * 365 + leap > offset) {
                year = year - 1;
                leap = year / 4 - year / 100 + year / 400;
            }
            offset = offset - (year * 365 + leap);

            int i = 12;
            while (offset < 367 * i / 12 - 30) {
                i--;
            }
            offset = offset - (367 * i / 12 - 30);
            mon = i + 2;
            if (mon > 12) {
                mon = mon - 12;
                year = year + 1;
            }

            day = (int) (offset + 1);
        }

        c.set(Calendar.YEAR, year);
        c.set(Calendar.MONTH, mon - 1);
        c.set(Calendar.DAY_OF_MONTH, day);
        c.set(Calendar.HOUR_OF_DAY, hour);
        c.set(Calendar.MINUTE, min);
        c.set(Calendar.SECOND, sec);
        c.set(Calendar.MILLISECOND, 0);

        return c.getTime();
    }

    // RFC 822 Date Format
    private static final String RFC822_DATE_FORMAT = "EEE, dd MMM yyyy HH:mm:ss z";

    /**
     * Formats Date to GMT string.
     *
     * @param date
     * @return
     */
    public static String formatRfc822Date(Date date) {
        return getRfc822DateFormat().format(date);
    }

    /**
     * Parses a GMT-format string.
     *
     * @param dateString
     * @return
     * @throws ParseException
     */
    public static Date parseRfc822Date(String dateString) throws ParseException {
        return getRfc822DateFormat().parse(dateString);
    }

    private static DateFormat getRfc822DateFormat() {
        SimpleDateFormat rfc822DateFormat = new SimpleDateFormat(
                RFC822_DATE_FORMAT, Locale.US);
        rfc822DateFormat.setTimeZone(new SimpleTimeZone(0, "GMT"));

        return rfc822DateFormat;
    }
}
